tolua源码分析(一) tolua的初始化流程

您所在的位置:网站首页 unity luajit tolua源码分析(一) tolua的初始化流程

tolua源码分析(一) tolua的初始化流程

#tolua源码分析(一) tolua的初始化流程| 来源: 网络整理| 查看: 265

tolua是一个Unity静态绑定lua代码的解决方案,它能够反射分析C#代码,生成C#包装类。它大大简化了C#代码和lua代码之间的集成,可以自动生成用于在Lua中访问Unity的绑定代码,并把C#中的常量、变量、函数、属性、类以及枚举暴露给Lua。简单来说就是tolua实现了一套方案,通过这个方案我们可以透明地在lua层调用C#的函数,也可以反过来从C#端调用lua的函数。这个项目是开源的,它的源代码部署在GitHub上:

tolua

tolua_runtime

这里有两个项目,是因为解决方案除了需要在C#端实现逻辑以外,还需要修改lua虚拟机,在C层扩展lua虚拟机的接口,完成C#对象的管理,以及接口的转发。那么,实际使用过程中,一般项目的业务代码架构如下图所示:

tolua源码分析(一) 业务代码架构

这里,tolua在大多数平台上使用的lua虚拟机并非是官方lua,而是LuaJIT,目前只支持lua5.1的语法。当然我们也可以对源码进行修改,替换lua虚拟机为官方lua的高版本。从图中也可以看出,tolua runtime是非常重要的中间层,它扩展了lua虚拟机,让lua和C#的交互畅通无阻。

我们来看一下官方给的第一个例子HelloWorld,这个例子很简单,就是演示了如何在C#端执行一段lua代码:

using UnityEngine; using LuaInterface; using System; public class HelloWorld : MonoBehaviour { void Awake() { LuaState lua = new LuaState(); lua.Start(); string hello = @" print('hello tolua#') "; lua.DoString(hello, "HelloWorld.cs"); lua.CheckTop(); lua.Dispose(); lua = null; } }

这段代码中,第9-10行是用来初始化tolua本身的,这里的LuaState数据结构表示lua虚拟机;第11-16行就是加载一段代码并执行,DoString函数与lua自带的loadstring函数功能类似,第一个参数是要执行的lua代码块字符串,第二个参数是代码块的名称chunkName,该参数主要用于调试。第17行的CheckTop函数用来检查当前lua虚拟机的栈顶状态,如果栈中还有元素残留,说明虚拟机出现了问题,很可能是某个自定义扩展lua的函数所导致。第18-19行是对LuaState数据结构进行销毁清理。

tolua的GitHub上也附带了Unity工程,我们尝试运行下HelloWorld这个例子,可以看到输出如下:

tolua源码分析(一) HelloWorld

控制台打印了初始化lua虚拟机的时间,以及LuaJIT的版本号和状态,操作系统的信息,还有我们执行的lua代码,最后是lua虚拟机销毁的log。

有了大概的了解之后,我们现在深入地去看初始化相关的两个函数。首先是LuaState的构造函数,长这样:

public LuaState() { if (mainState == null) { mainState = this; // MULTI_STATE Not Support injectionState = mainState; } float time = Time.realtimeSinceStartup; InitTypeTraits(); InitStackTraits(); L = LuaNewState(); LuaException.Init(L); stateMap.Add(L, this); OpenToLuaLibs(); ToLua.OpenLibs(L); OpenBaseLibs(); LuaSetTop(0); InitLuaPath(); Debugger.Log("Init lua state cost: {0}", Time.realtimeSinceStartup - time); }

从这些函数的名字来推断,LuaState的构造过程大致可以分为以下几个部分:

初始化各种traits,这个traits究竟是啥我们等下再说;创建lua虚拟机;加载各种lua库,可能来自C#,C,甚至lua;初始化lua代码的加载路径

那么现在我们来说一下这个traits。熟悉C++的同学肯定会立马想到type_traits,是的,这里这两个函数的作用与之类似,通过C#的泛型,在编译期就完成了不同类型检查函数,转换函数的绑定。

void InitTypeTraits() { LuaMatchType _ck = new LuaMatchType(); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckNumber); TypeTraits.Init(_ck.CheckBool); TypeTraits.Init(_ck.CheckLong); TypeTraits.Init(_ck.CheckULong); TypeTraits.Init(_ck.CheckString); ... } void InitStackTraits() { LuaStackOp op = new LuaStackOp(); StackTraits.Init(op.Push, op.CheckSByte, op.ToSByte); StackTraits.Init(op.Push, op.CheckByte, op.ToByte); StackTraits.Init(op.Push, op.CheckInt16, op.ToInt16); StackTraits.Init(op.Push, op.CheckUInt16, op.ToUInt16); StackTraits.Init(op.Push, op.CheckChar, op.ToChar); StackTraits.Init(op.Push, op.CheckInt32, op.ToInt32); StackTraits.Init(op.Push, op.CheckUInt32, op.ToUInt32); StackTraits.Init(op.Push, op.CheckDecimal, op.ToDecimal); StackTraits.Init(op.Push, op.CheckFloat, op.ToFloat); StackTraits.Init(LuaDLL.lua_pushnumber, LuaDLL.luaL_checknumber, LuaDLL.lua_tonumber); StackTraits.Init(LuaDLL.lua_pushboolean, LuaDLL.luaL_checkboolean, LuaDLL.lua_toboolean); StackTraits.Init(LuaDLL.tolua_pushint64, LuaDLL.tolua_checkint64, LuaDLL.tolua_toint64); StackTraits.Init(LuaDLL.tolua_pushuint64, LuaDLL.tolua_checkuint64, LuaDLL.tolua_touint64); StackTraits.Init(LuaDLL.lua_pushstring, ToLua.CheckString, ToLua.ToString); ... }

TypeTraits和StackTraits都是泛型类,来看一下它们的Init函数:

public static class TypeTraits { static public Func Check = DefaultCheck; static public void Init(Func check) { if (check != null) { Check = check; } } } public static class StackTraits { static public Action Push = SelectPush(); static public Func Check = DefaultCheck; static public Func To = DefaultTo; static public void Init(Action push, Func check, Func to) { if (push != null) { Push = push; } if (to != null) { To = to; } if (check != null) { Check = check; } } }

从函数实现可以了解到,如果某个类型没有调用Init函数进行初始化,那么这两个泛型类会为该类型提供默认的函数。这个感觉是不是有点像C++的类模板?某些特定的类型,需要对类模板进行特化/偏特化处理,只不过C#要灵活得多。通过这种方式,我们就可以直接在外部根据不同类型,动态地增加/删除/修改对应绑定的函数,极大增加了代码的灵活性。

观察TypeTraits的Init实现,可以知道它的Check函数是一个接受两个参数(IntPtr,int),返回类型为bool的函数。我们来看一下int类型对应的CheckNumber函数:

public bool CheckNumber(IntPtr L, int pos) { return LuaDLL.lua_type(L, pos) == LuaTypes.LUA_TNUMBER; }

这个函数很简单,就是判断lua栈上pos位置的元素类型是否为number。由于lua没有像C#那样提供那么多种数值类型,因此我们看到有很多C#数值类型的Init使用的是CheckNumber。那么不妨让我们大胆地猜测一下,TypeTraits的Check函数是用来判断lua栈上pos位置的元素类型是否为T。

类似地,可以知道StackTraits拥有3个函数,其中Push函数接受两个参数(IntPtr,T),返回类型为void;Check函数接受两个参数(IntPtr,int),返回类型为T;To函数接受两个参数(IntPtr,int),返回类型为T。同样我们看一下int类型对应的处理:

public void Push(IntPtr L, int n) { LuaDLL.lua_pushnumber(L, n); } public int CheckInt32(IntPtr L, int stackPos) { double ret = LuaDLL.luaL_checknumber(L, stackPos); return Convert.ToInt32(ret); } public int ToInt32(IntPtr L, int stackPos) { double ret = LuaDLL.lua_tonumber(L, stackPos); return Convert.ToInt32(ret); }

那么,大胆地猜测,StackTraits的Push函数是用来把类型T的元素压入lua栈中;Check函数对栈上pos位置的元素进行检查,并尝试转换为T类型的值返回,可能会抛出异常;To函数对栈上pos位置的元素直接进行类型转换,返回T类型的值,不会抛出异常,如果转换失败返回T类型的默认值。

下一步就是创建lua虚拟机了,这块没有什么好说的,直接调用luaL_newstate即可。

接下来是这一句LuaException.Init(L);,它做的事情比较简单,就是为LuaException类指定了当前项目的路径,这个类顾名思义,是用来处理lua异常堆栈用的。

public static void Init(IntPtr L0) { L = L0; Type type = typeof(StackTraceUtility); FieldInfo field = type.GetField("projectFolder", BindingFlags.Static | BindingFlags.GetField | BindingFlags.NonPublic); LuaException.projectFolder = (string)field.GetValue(null); projectFolder = projectFolder.Replace('\\', '/'); #if DEVELOPER Debugger.Log("projectFolder is {0}", projectFolder); #endif }

然后就到了重头戏,加载各种lua库的代码了。首先是OpenToLuaLibs,定义如下:

public void OpenToLuaLibs() { LuaDLL.tolua_openlibs(L); LuaOpenJit(); }

tolua_openlibs是一个C函数,它的实现可以在tolua_runtime这里找到,位于tolua.c文件中。这里提一下,tolua.c基本上放了所有对lua虚拟机进行扩展的函数,主要是用在和unity,也就是C#层交互中。

LUALIB_API void tolua_openlibs(lua_State *L) { initmodulebuffer(); luaL_openlibs(L); int top = lua_gettop(L); tolua_setluabaseridx(L); tolua_opentraceback(L); tolua_openpreload(L); tolua_openubox(L); tolua_openfixedmap(L); tolua_openint64(L); tolua_openuint64(L); tolua_openvptr(L); //tolua_openrequire(L); luaL_register(L, "Mathf", tolua_mathf); luaL_register(L, "tolua", tolua_funcs); lua_getglobal(L, "tolua"); lua_pushstring(L, "gettag"); lua_pushlightuserdata(L, &gettag); lua_rawset(L, -3); lua_pushstring(L, "settag"); lua_pushlightuserdata(L, &settag); lua_rawset(L, -3); lua_pushstring(L, "version"); lua_pushstring(L, "1.0.7"); lua_rawset(L, -3); lua_settop(L,top); }

我们先来看一下函数的声明。LUALIB_API是一个宏,在不同的编译环境下定义不同:

#define LUALIB_API LUA_API /* Linkage of public API functions. */ #if defined(LUA_BUILD_AS_DLL) #if defined(LUA_CORE) || defined(LUA_LIB) #define LUA_API __declspec(dllexport) #else #define LUA_API __declspec(dllimport) #endif #else #define LUA_API extern #endif

函数的返回类型为void,说明该函数返回时,不会在lua虚拟机栈上存放任何值。这也是函数实现中,一开始调用lua_gettop记录栈顶,然后最后调用lua_settop恢复栈顶的原因。函数相对来说比较长,大概可以分为以下几个部分:

加载lua的标准C库初始化tolua需要的各种全局环境加载tolua需要的扩展C库设置tolua的标志信息,恢复lua虚拟机栈

luaL_openlibs就是加载lua自带的各种C库函数,对于tolua默认使用的LuaJIT来说,有如下这些:

static const luaL_Reg lj_lib_load[] = { { "", luaopen_base }, { LUA_LOADLIBNAME, luaopen_package }, { LUA_TABLIBNAME, luaopen_table }, { LUA_IOLIBNAME, luaopen_io }, { LUA_OSLIBNAME, luaopen_os }, { LUA_STRLIBNAME, luaopen_string }, { LUA_MATHLIBNAME, luaopen_math }, { LUA_DBLIBNAME, luaopen_debug }, { LUA_BITLIBNAME, luaopen_bit }, { LUA_JITLIBNAME, luaopen_jit }, { NULL, NULL } }; static const luaL_Reg lj_lib_preload[] = { #if LJ_HASFFI { LUA_FFILIBNAME, luaopen_ffi }, #endif { NULL, NULL } };

初始化tolua环境这一部分比较复杂,我们一个函数一个函数地过。首先是tolua_setluabaseridx函数:

void tolua_setluabaseridx(lua_State *L) { for (int i = 1; i 2; i--) { LuaDLL.lua_rawgeti(L, -2, i - 1); LuaDLL.lua_rawseti(L, -3, i); } LuaDLL.lua_rawseti(L, -2, 2); LuaDLL.lua_pop(L, 2); }

package.loaders主要用在lua的require函数中,它会使用loaders里的load函数加载指定模块。函数第7-13行的主要作用就是将C#层自定义的Loader函数插入到原有的package.loaders的第2个位置中。这个函数接管了lua文件加载的逻辑,可以方便我们使用项目自身加载文件的方式来管理lua文件。可能有人会问,为什么不是插入到表头呢?答案是第1个位置的lua loader叫做preload,它使用package.preload这个内部table,这个table的key是加载模块的名称,value是加载函数。也就是说,它是一种更特殊的loader,会根据模块名而启动特殊的加载逻辑。那当然C#的Loader函数要为它让步啦。

这里也贴一下官方的解释:

require (modname) Loads the given module. The function starts by looking into the package.loaded table to determine whether modname is already loaded. If it is, then require returns the value stored at package.loaded[modname]. Otherwise, it tries to find a loader for the module. To find a loader, require is guided by the package.loaders array. By changing this array, we can change how require looks for a module. The following explanation is based on the default configuration for package.loaders. First require queries package.preload[modname]. If it has a value, this value (which should be a function) is the loader. Otherwise require searches for a Lua loader using the path stored in package.path. If that also fails, it searches for a C loader using the path stored in package.cpath. If that also fails, it tries an all-in-one loader (see package.loaders). Once a loader is found, require calls the loader with a single argument, modname. If the loader returns any value, require assigns the returned value to package.loaded[modname]. If the loader returns no value and has not assigned any value to package.loaded[modname], then require assigns true to this entry. In any case, require returns the final value of package.loaded[modname]. If there is any error loading or running the module, or if it cannot find any loader for the module, then require signals an error.

OpenLibs里的剩下几个函数,这里就不一一展开了,第4-10行主要就是用C#重写了lua自带的panic,print,dofile,loadfile函数,重写它们的原因也是为了能够在C#层获得详细的日志和堆栈,以及可以在C#层控制lua的加载逻辑;第12-28行就是新增了tolua.isnull,tolua.typeof,tolua.tolstring,tolua.toarray一些辅助函数。再看第30-37行,我们先不用管这些函数调用的具体含义,最后的结果就是在lua层定义了一个tolua.null和null两个全局变量,这两个变量指向同一个userdata,这个userdata的含义就是C#的NullObject类。这个类是tolua自己定义的,就是一个空空如也的类,用来标记空对象:

public class NullObject { }

好了,现在可以回到LuaState的构造函数继续分析了。接下来看到的是OpenBaseLibs函数,这个函数实际上就是将常用的C#类注册到lua中,使得lua可以访问C#类的方法或属性,这个后面会展开来说;LuaSetTop(0)所做的事情就是清理当前lua虚拟机的堆栈,使其保持调用前的模样;InitLuaPath函数就是设置了加载lua文件的路径,也就是扩充了lua的package.path。

自此,包含tolua环境的lua虚拟机算是构造完成了。剩下的就是一个Start函数。这个函数中我们目前只需关注OpenBaseLuaLibs:

void OpenBaseLuaLibs() { DoFile("tolua.lua"); //tolua table名字已经存在了,不能用require LuaUnityLibs.OpenLuaLibs(L); }

可以看到这里又去加载了一个lua文件,不难发现,这个lua文件对一些可能会频繁调用,但又实现简单的C#方法,以及一些常用的值类型,改用lua实现了:

-- tolua.lua ... require "misc.functions" Mathf = require "UnityEngine.Mathf" Vector3 = require "UnityEngine.Vector3" Quaternion = require "UnityEngine.Quaternion" Vector2 = require "UnityEngine.Vector2" Vector4 = require "UnityEngine.Vector4" Color = require "UnityEngine.Color" Ray = require "UnityEngine.Ray" Bounds = require "UnityEngine.Bounds" RaycastHit = require "UnityEngine.RaycastHit" Touch = require "UnityEngine.Touch" LayerMask = require "UnityEngine.LayerMask" Plane = require "UnityEngine.Plane" Time = reimport "UnityEngine.Time" list = require "list" utf8 = require "misc.utf8" require "event" require "typeof" require "slot" require "System.Timer" require "System.coroutine" require "System.ValueType" require "System.Reflection.BindingFlags" ...

LuaUnityLibs.OpenLuaLibs分为两步,第一步的tolua_openlualibs对lua实现的C#方法和类型,进行了注册,这样做的目的是使得lua和C#互相调用时,这些方法和类型会自动转换。例如C#传入一个C#版本的Vector3给lua时,lua层接收到的实际上是lua版本的Vector3,而不是一个userdata;第二步的SetOutMethods为这些类型设置了out方法,可以让lua层调用C#层的带有out参数的方法。

public static void OpenLuaLibs(IntPtr L) { if (LuaDLL.tolua_openlualibs(L) != 0) { string error = LuaDLL.lua_tostring(L, -1); LuaDLL.lua_pop(L, 1); throw new LuaException(error); } SetOutMethods(L, "Vector3", GetOutVector3); ... }

自此,tolua的初始化流程算是被我们拆解完毕了。总结来说,初始化主要分为两大块内容,一是进行各种配置,例如修改了若干原生lua方法,缓存了各种变量,创建了tolua所需的各种数据;二是加载了各种tolua用的库,这些库既可能来自C,也可能来自C#,甚至从lua频繁调用的角度考虑,来自lua。

下一节我们将关注C#如何调用lua函数的实现机制。

如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊

Reference

[1] Lua 5.1 Reference Manual

[2] The LuaJIT Project

[3] tolua

[4] tolua_runtime

[5] Lua的require小结

[6] 再探Lua的require



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3